HEEx (HTML + EEx) is Phoenix LiveView’s template language that combines HTML with embedded Elixir. It provides HTML-aware interpolation, component support, compile-time validation, and smart change tracking.
The ~H Sigil
HEEx templates are created using the ~H sigil:
def render (assigns) do
~H"""
< div >
< h1 > {@title} </ h1 >
< p > Welcome, {@name}! </ p >
</ div >
"""
end
The ~H sigil requires an assigns variable to be in scope. This variable must be a map containing all the data your template needs.
External Template Files
For larger templates, use .html.heex files instead of inline templates:
lib/my_app_web/live/
├── user_live.ex
└── user_live.html.heex
If you don’t define render/1, LiveView automatically looks for a .html.heex file with the same name.
Interpolation
Basic Interpolation
Use {...} for HTML-aware interpolation:
<p>Hello, {@name}!</p>
<h1>{@page_title}</h1>
<span>{calculate_total(@items)}</span>
In Attributes
<div class={@css_class}>
<img src={@avatar_url} alt={@user.name}>
<a href={~p"/users/#{@user.id}"}>
String Interpolation in Attributes
<div class={"btn btn-#{@type}"}>
<span id={"user-#{@user.id}-status"}>
Special Attribute Values
true
false/nil
Lists (class only)
Renders the attribute without a value: <input required={true}>
<!-- Renders: <input required> -->
Omits the attribute entirely: <input required={false}>
<!-- Renders: <input> -->
<div class={["btn", @active && "active", @size]}>
<!-- Automatically filters nil/false -->
class and style attributes render as empty strings instead of being omitted when false/nil, which has the same effect but enables rendering optimizations.
Dynamic Attributes
Pass multiple attributes as a map or keyword list:
<div {@dynamic_attrs}>
Content
</div>
assign (socket, :dynamic_attrs , [
class: "card" ,
id: "main" ,
"data-user-id" : @user .id
])
The expression inside {@dynamic_attrs} must be a keyword list or map with atom keys .
Block Expressions
For multi-line Elixir code, use <%= ... %>:
<%= if @show_greeting? do %>
<p>Hello, {@name}!</p>
<% end %>
<%= for item <- @items do %>
<div>{item.title}</div>
<% end %>
<%= case @status do %>
<% :active -> %>
<span class="badge-green">Active</span>
<% :inactive -> %>
<span class="badge-gray">Inactive</span>
<% end %>
Use <%= %> when the block produces output, and <% %> for control flow keywords like end, else, etc.
Special Attributes
:if - Conditional Rendering
Conditionally render elements:
<div :if={@user}>Welcome, {@user.name}!</div>
<p :if={@show_message}>This is a message</p>
<button :if={@can_edit?}>Edit</button>
Works on any HTML element, component, or slot:
<.button :if={@show_save}>Save</.button>
<:action :if={@can_delete}>
<button>Delete</button>
</:action>
:for - List Iteration
Iterate over collections:
<ul>
<li :for={item <- @items}>{item.name}</li>
</ul>
<.card :for={post <- @posts} title={post.title}>
{post.body}
</.card>
With Pattern Matching
<div :for={{key, value} <- @map}>
{key}: {value}
</div>
<div :for={%{name: name, age: age} <- @users}>
{name} is {age} years old
</div>
:key for Change Tracking
By default, LiveView tracks items by index. Use :key for better change tracking:
<li :for={item <- @items} :key={item.id}>
{item.name}
</li>
Without :key, inserting an item at the beginning causes all subsequent items to be re-rendered. With :key, only the new item is sent to the client.
:key has no effect when using streams (Phoenix.LiveView.stream/4).
Combining :if and :for
<div :for={user <- @users} :if={user.active?}>
{user.name}
</div>
<!-- Equivalent to: -->
<%= for user <- @users, user.active? do %>
<div>{user.name}</div>
<% end %>
HEEx’s :for does not support multiple generators in one expression. For that, use EEx blocks: <%= for x <- @list1, y <- @list2 do %>
<div>{x} - {y}</div>
<% end %>
:let - Slot Variables
Capture values passed from components:
<.form :let={f} for={@form}>
<.input field={f[:name]} />
</.form>
<.list :let={item} entries={@items}>
<strong>{item.title}</strong>
</.list>
See the Components guide for more on slots and :let.
Function Components
Invoke function components with HTML-like syntax:
Local Components
<.button type="submit">Save</.button>
<.card title="Welcome" />
<.icon name="check" />
Remote Components
<MyApp.Components.button>Click me</MyApp.Components.button>
<Phoenix.Component.link href="/">Home</Phoenix.Component.link>
With Attributes
<.user_card
name={@user.name}
email={@user.email}
avatar={@user.avatar}
/>
<!-- Self-closing (no inner content) -->
<.icon name="star" />
<!-- Block form (with inner content) -->
<.button>
<.icon name="save" /> Save
</.button>
Comments that don’t appear in rendered HTML:
<%!-- This is a HEEx comment -->
<%!-- It won't appear in the HTML sent to the browser -->
Regular HTML comments are preserved:
<!-- This is an HTML comment -->
<!-- It will appear in the rendered HTML -->
Escaping Curly Braces
If you need literal { or } in text:
<!-- Use HTML entities -->
<p>This is a {curly brace}</p>
<!-- Or EEx expressions -->
<p>This is a <%= "{" %>curly brace<%= "}" %></p>
Curly brace interpolation is disabled inside <script> and <style> tags:
<script>
// Use <%= %> for interpolation here
const userId = <%= @user.id %>;
const apiUrl = "<%= @api_url %>";
</script>
<style>
.theme-color {
color: <%= @theme_color %>;
}
</style>
Disabling Interpolation
Use phx-no-curly-interpolation to disable {...} in any tag:
<div phx-no-curly-interpolation>
{This won't be interpolated}
</div>
HEEx templates support automatic formatting via mix format:
# .formatter.exs
[
inputs: [
"*.{heex,ex,exs}" ,
"{config,lib,test}/**/*.{heex,ex,exs}"
]
]
Format your code:
Use the noformat modifier:
~H"""
noformat
< div >
This wonot be formatted
</ div >
"""
Debug Annotations
Enable debug annotations to see where components are rendered:
# config/dev.exs
config :phoenix_live_view ,
debug_heex_annotations: true ,
debug_attributes: true
With debug_heex_annotations: true:
<!-- @caller lib/app_web/live/home_live.ex:20 -->
<!-- <AppWeb.Components.header> lib/app_web/components.ex:123 -->
< header class = "p-5" >
<!-- @caller lib/app_web/live/home_live.ex:48 -->
<!-- <AppWeb.Components.button> lib/app_web/components.ex:456 -->
< button class = "px-2 bg-indigo-500" > Click </ button >
<!-- </AppWeb.Components.button> -->
</ header >
<!-- </AppWeb.Components.header> -->
Debug Attributes
With debug_attributes: true:
< header data-phx-loc = "125" class = "p-5" >
< button data-phx-loc = "458" class = "px-2" > Click </ button >
</ header >
These options require mix clean and a full recompile to take effect.
Template Best Practices
Avoid defining variables in templates
Don’t do this: <% total = @x + @y %>
<p>Total: {total}</p>
Do this instead: def render (assigns) do
assigns = assign (assigns, :total , assigns.x + assigns.y)
~H"<p>Total: {@total}</p>"
end
Variables disable change tracking!
Keep logic out of templates
Don’t do this: <p>Price: {if @discount, do: @price * 0.9, else: @price}</p>
Do this instead: def render (assigns) do
assigns = assign (assigns, :final_price , calculate_price (assigns))
~H"<p>Price: {@final_price}</p>"
end
defp calculate_price (%{ discount: true , price: price}), do: price * 0.9
defp calculate_price (%{ price: price}), do: price
Never access the assigns variable directly
Don’t do this: Do this instead: Direct access to assigns disables change tracking.
Don't perform data loading in templates
Common Patterns
Conditional Classes
<div class={[
"card",
@active && "card-active",
@large && "card-lg",
@color
]}>
Rendering Lists with Empty State
<div :if={@items == []}>
<p>No items found</p>
</div>
<div :if={@items != []}>
<div :for={item <- @items}>
{item.name}
</div>
</div>
Tables
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr :for={user <- @users}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<button phx-click="edit" phx-value-id={user.id}>Edit</button>
</td>
</tr>
</tbody>
</table>
<.form for={@form} phx-submit="save" phx-change="validate">
<.input field={@form[:name]} label="Name" required />
<.input field={@form[:email]} label="Email" type="email" />
<div class="actions">
<.button type="submit">Save</.button>
<.link href={~p"/cancel"}>Cancel</.link>
</div>
</.form>
Loading States
<div :if={@loading} class="spinner">
Loading...
</div>
<div :if={!@loading}>
<div :for={item <- @items}>
{item.title}
</div>
</div>
Error Messages
<div :if={@errors != []} class="alert alert-error">
<p :for={error <- @errors}>{error}</p>
</div>
Modal Dialogs
<div :if={@show_modal} class="modal-backdrop" phx-click="close_modal">
<div class="modal" phx-click-away="close_modal">
<div class="modal-header">
<h2>{@modal_title}</h2>
<button phx-click="close_modal">×</button>
</div>
<div class="modal-body">
{@modal_content}
</div>
</div>
</div>
HEEx templates are compiled to efficient Elixir code:
Static parts are computed once at compile time
Dynamic parts are only re-computed when their assigns change
Change tracking minimizes data sent over the wire
Diffs are computed automatically and sent to the client
What Gets Tracked?
<div id={"user-#{@user.id}"}>
{@user.name}
</div>
If only @user.name changes:
@user.id is not re-evaluated
Only the @user.name text is sent to the client
The id attribute is not sent again
Summary
Use ~H sigil for inline templates or .html.heex files for external templates
Interpolate with {@assign} for values and {expression} for computed values
Use :if, :for, and :key for control flow
Invoke components with <.component> syntax
Avoid variables and direct data access in templates
Keep complex logic in functions, not templates
Use :key with :for for efficient list updates
Enable debug annotations during development